import re
import urllib
import operator

from webob import Request
from pkg_resources import iter_entry_points

from pydap.model import *
from pydap.handlers.lib import BaseHandler


def make_middleware(app, global_conf, **kwargs):
    return FunctionMiddleware(app, global_conf=global_conf, **kwargs)


class FunctionMiddleware(object):

    functions = dict((r.name, r.load())
            for r in iter_entry_points('pydap.function'))

    def __init__(self, app, **kwargs):
        self.app = app

    def __call__(self, environ, start_response):
        # Rewrite the constraint expression.
        environ['x-wsgiorg.want_parsed_response'] = True
        query = environ.pop('QUERY_STRING')
        environ['QUERY_STRING'] = rewrite(query)

        # Try to recover the dataset from the response, using the
        # "avoiding serialization" specification from wsgi.org
        # (http://wsgi.org/wsgi/Specifications/avoiding_serialization)
        req = Request(environ)
        res = req.get_response(self.app)
        method = getattr(res.app_iter, 'x_wsgiorg_parsed_response', False)
        if not method or not query: return res(environ, start_response)

        modified = method(DatasetType)               
        dataset = DatasetType(name=modified.name, attributes=modified.attributes)
        tokens = list(tokenize(query))
        while tokens:
            token = tokens.pop(0)
            # If a variable was requested, simply copy it from the
            # modified dataset.
            if isinstance(token, VARIABLE):
                names = re.sub('\[.*?\]', '', str(token)).split('.')
                target, template = dataset, modified
                while names:
                    name = names.pop(0)
                    if name not in target:
                        target[name] = template[name]
                    target, template = target[name], template[name]
            # If this is a function call, replace it with the
            # corresponding variable.
            elif isinstance(token, FUNCTION):
                var = eval_func(token, tokens, modified, self.functions)
                dataset[var.name] = var

        # Return the original response (DDS, DAS, etc.)
        response = BaseHandler.response_map[ environ['pydap.response'] ]
        responder = response(dataset)
        return responder(environ, start_response)


def eval_func(token, tokens, dataset, functions):
    """
    Evaluate a function call.

    """
    func = functions[str(token)]
    args = []
    count = 0
    while not (isinstance(token, CLOSE) and count == 0):
        token = tokens.pop(0)
        if isinstance(token, OPEN):
            count += 1
        elif isinstance(token, CLOSE):
            count -= 1
        elif isinstance(token, STRING):
            args.append(str(token))
        elif isinstance(token, NUMBER):
            args.append(float(str(token)))
        elif isinstance(token, VARIABLE):
            names = [dataset] + re.sub('\[.*?\]', '', str(token)).split('.')
            var = reduce(operator.getitem, names)
            args.append(var)
        elif isinstance(token, FUNCTION):
            args.append(eval_func(token, tokens, dataset, functions))

    return func(*args)


def rewrite(query):
    """
    Rewrite the query string.

    Variables that are arguments to function calls should be
    passed to the server as a normal request; later they'll be
    removed from the dataset.

    """
    projection = []
    selection = [urllib.unquote(token) for token in query.split('&')]
    if selection and not re.search('<=|>=|!=|=~|>|<|=', selection[0]):
        projection = [p for p in selection.pop(0).split(',') if p]

    # calculate new projection
    new_projection = []
    for request in projection + selection:
        for var in tokenize(request, VARIABLE):
            new_projection.append(str(var))

    params = [','.join(new_projection)] + selection
    return '&'.join(p for p in params if p)


class TOKEN(object):
    regexp = re.compile('.*')

    def __init__(self, input):
        self.input = input  

    def __str__(self):
        return self.input

    @classmethod
    def match(cls, input):
        m = cls.regexp.match(input)
        if m: return m.group(0)


class FUNCTION(TOKEN):
    regexp = re.compile(r'([\w_\.%]+)\(')

    @classmethod
    def match(cls, input):
        m = cls.regexp.match(input)
        if m: return m.group(1)


class VARIABLE(TOKEN):
    regexp = re.compile(r'[A-Za-z][\w_\.%]*(\[\d+(:\d+)*\])*')


class STRING(TOKEN):
    regexp = re.compile(r'".+?[^\\]"')


class NUMBER(TOKEN):
    regexp = re.compile("""
        [+-]?         # first, match an optional sign
        (             # then match integers or f.p. mantissas:
            \d+       # start out with a ...
            (
                \.\d* # mantissa of the form a.b or a.
            )?        # ? takes care of integers of the form a
           |\.\d+     # mantissa of the form .b
        )
        ([eE][+-]?\d+)?  # finally, optionally match an exponent
        """, re.VERBOSE)


class OPEN(TOKEN):
    regexp = re.compile('\(')


class CLOSE(TOKEN):
    regexp = re.compile('\)')


class OPERATOR(TOKEN):
    regexp = re.compile('<=|>=|!=|=~|>|<|=')


class COMMA(TOKEN):
    regexp = re.compile(',')


class WHITESPACE(TOKEN):
    regexp = re.compile('(\s|%20)+')


def tokenize(input, type_=object):
    """
    A simple tokenizer.

    """
    Tokens = [FUNCTION, VARIABLE, STRING, NUMBER, OPEN, CLOSE, OPERATOR, COMMA, WHITESPACE]
    while input:
        for Token in Tokens:
            m = Token.match(input)
            if m:
                input = input[len(m):]
                token = Token(m)
                if isinstance(token, type_):
                    yield token
                break 
        else:
            raise StopIteration
